/**
 * @license
 * Copyright 2017 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import {AuthRequestInit, Finalizable} from '../../types/types';
import {fire} from '../../utils/event-util';
import {getBaseUrl} from '../../utils/url-util';
import {AuthService} from './gr-auth';

const MAX_AUTH_CHECK_WAIT_TIME_MS = 1000 * 30; // 30s

const CREDS_EXPIRED_MSG = 'Credentials expired.';

// visible for testing
export enum AuthStatus {
  UNDETERMINED = 0,
  AUTHED = 1,
  NOT_AUTHED = 2,
  ERROR = 3,
}

interface AuthRequestInitWithHeaders extends AuthRequestInit {
  // RequestInit define headers as optional property with a type
  // Headers | string[][] | Record<string, string>
  // In Auth class headers property is always set and has type Headers
  headers: Headers;
}

/**
 * Auth class.
 */
export class Auth implements AuthService, Finalizable {
  private authCheckPromise?: Promise<boolean>;

  private _last_auth_check_time: number = Date.now();

  private _status = AuthStatus.UNDETERMINED;

  finalize() {}

  /**
   * Returns if user is authed or not.
   */
  authCheck(): Promise<boolean> {
    if (
      !this.authCheckPromise ||
      Date.now() - this._last_auth_check_time > MAX_AUTH_CHECK_WAIT_TIME_MS
    ) {
      // Refetch after last check expired
      this.authCheckPromise = fetch(`${getBaseUrl()}/auth-check`)
        .then(res => {
          // Make a call that requires loading the body of the request. This makes it so that the browser
          // can close the request even though callers of this method might only ever read headers.
          // See https://stackoverflow.com/questions/45816743/how-to-solve-this-caution-request-is-not-finished-yet-in-chrome
          try {
            res.clone().text();
          } catch {
            // Ignore error
          }

          // auth-check will return 204 if authed
          // treat the rest as unauthed
          if (res.status === 204) {
            this._setStatus(AuthStatus.AUTHED);
            return true;
          } else {
            this._setStatus(AuthStatus.NOT_AUTHED);
            return false;
          }
        })
        .catch(() => {
          this._setStatus(AuthStatus.ERROR);
          // Reset authCheckPromise to avoid caching the failed promise
          this.authCheckPromise = undefined;
          return false;
        });
      this._last_auth_check_time = Date.now();
    }

    return this.authCheckPromise;
  }

  clearCache() {
    this.authCheckPromise = undefined;
  }

  private _setStatus(status: AuthStatus) {
    if (this._status === status) return;

    if (this._status === AuthStatus.AUTHED) {
      fire(document, 'auth-error', {
        message: CREDS_EXPIRED_MSG,
        action: 'Refresh credentials',
      });
    }
    this._status = status;
  }

  // visible for testing
  get status() {
    return this._status;
  }

  get isAuthed() {
    return this._status === AuthStatus.AUTHED;
  }

  /**
   * Perform network fetch with authentication.
   */
  fetch(url: string, options?: AuthRequestInit): Promise<Response> {
    const optionsWithHeaders: AuthRequestInitWithHeaders = {
      headers: new Headers(),
      ...options,
    };
    return this._fetchWithXsrfToken(url, optionsWithHeaders);
  }

  // private but used in test
  _getCookie(name: string): string {
    const key = name + '=';
    let result = '';
    document.cookie.split(';').some(c => {
      c = c.trim();
      if (c.startsWith(key)) {
        result = c.substring(key.length);
        return true;
      }
      return false;
    });
    return result;
  }

  private _fetchWithXsrfToken(
    url: string,
    options: AuthRequestInitWithHeaders
  ): Promise<Response> {
    if (options.method && options.method !== 'GET') {
      const token = this._getCookie('XSRF_TOKEN');
      if (token) {
        options.headers.append('X-Gerrit-Auth', token);
      }
    }
    options.credentials = 'same-origin';
    return this._ensureBodyLoaded(fetch(url, options));
  }

  private _ensureBodyLoaded(response: Promise<Response>): Promise<Response> {
    return response.then(response => {
      if (!response.ok) {
        // Make a call that requires loading the body of the request. This makes it so that the browser
        // can close the request even though callers of this method might only ever read headers.
        // See https://stackoverflow.com/questions/45816743/how-to-solve-this-caution-request-is-not-finished-yet-in-chrome
        try {
          response.clone().text();
        } catch {
          // Ignore error
        }
      }
      return response;
    });
  }
}
